#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "vm.h"

const char *optr_to_str(int enum_value) {
  switch (enum_value) {
  case InsJtrue:
    return "InsJtrue";
  case InsJfalse:
    return "InsJfalse";
  case InsJmp:
    return "InsJmp";
  case InsMov:
    return "InsMov";
  case InsInit:
    return "InsInit";
  case InsAdd:
    return "InsAdd";
  case InsSub:
    return "InsSub";
  case InsMul:
    return "InsMul";
  case InsDiv:
    return "InsDiv";
  case InsMod:
    return "InsMod";
  case InsNop:
    return "InsNop";
  case InsJlt:
    return "InsJlt";
  case InsJle:
    return "InsJle";
  case InsJgt:
    return "InsJgt";
  case InsJge:
    return "InsJge";
  case InsJeq:
    return "InsJeq";
  case InsJne:
    return "InsJne";
  case InsOr:
    return "InsOr";
  case InsAnd:
    return "InsAnd";
  case InsNot:
    return "InsNot";
  case InsIn:
    return "InsIn";
  case InsOut:
    return "InsOut";
  case InsUminus:
    return "InsUminus";
  case InsCall:
    return "InsCall";
  case InsRet:
    return "InsRet";
  case InsSetBx:
    return "InsSetBx";
  case InsAddBx:
    return "InsAddBx";
  default:
    return "INSTRTYPE_UNKNOWN_ENUM_VALUE";
  }
}

int str_to_optr(const char *name) {
  if (0 == strcmp(name, "InsJtrue")) {
    return InsJtrue;
  }

  if (0 == strcmp(name, "InsJfalse")) {
    return InsJfalse;
  }

  if (0 == strcmp(name, "InsJmp")) {
    return InsJmp;
  }

  if (0 == strcmp(name, "InsMov")) {
    return InsMov;
  }

  if (0 == strcmp(name, "InsInit")) {
    return InsInit;
  }

  if (0 == strcmp(name, "InsAdd")) {
    return InsAdd;
  }

  if (0 == strcmp(name, "InsSub")) {
    return InsSub;
  }

  if (0 == strcmp(name, "InsMul")) {
    return InsMul;
  }

  if (0 == strcmp(name, "InsDiv")) {
    return InsDiv;
  }

  if (0 == strcmp(name, "InsMod")) {
    return InsMod;
  }

  if (0 == strcmp(name, "InsNop")) {
    return InsNop;
  }

  if (0 == strcmp(name, "InsJlt")) {
    return InsJlt;
  }

  if (0 == strcmp(name, "InsJle")) {
    return InsJle;
  }

  if (0 == strcmp(name, "InsJgt")) {
    return InsJgt;
  }

  if (0 == strcmp(name, "InsJge")) {
    return InsJge;
  }

  if (0 == strcmp(name, "InsJeq")) {
    return InsJeq;
  }

  if (0 == strcmp(name, "InsJne")) {
    return InsJne;
  }

  if (0 == strcmp(name, "InsOr")) {
    return InsOr;
  }

  if (0 == strcmp(name, "InsAnd")) {
    return InsAnd;
  }

  if (0 == strcmp(name, "InsNot")) {
    return InsNot;
  }

  if (0 == strcmp(name, "InsIn")) {
    return InsIn;
  }

  if (0 == strcmp(name, "InsOut")) {
    return InsOut;
  }

  if (0 == strcmp(name, "InsUminus")) {
    return InsUminus;
  }

  if (0 == strcmp(name, "InsCall")) {
    return InsCall;
  }

  if (0 == strcmp(name, "InsRet")) {
    return InsRet;
  }

  if (0 == strcmp(name, "InsSetBx")) {
    return InsSetBx;
  }

  if (0 == strcmp(name, "InsAddBx")) {
    return InsAddBx;
  }

  fprintf(stderr, "%s", name);
  assert(false);
  return 0;
}

const char *inst_to_str(const Instruction *inst) {
  static char inst_str[256];
  snprintf(inst_str, sizeof(inst_str), "%s %d %d %d",
           optr_to_str(inst->optr) + 3, inst->arg1, inst->arg2, inst->result);
  return inst_str;
}

VirtualMachine *vm_create() {
  VirtualMachine *vm = (VirtualMachine *)calloc(1, sizeof(VirtualMachine));
  vm->cs_index = 2;
  return vm;
}

void vm_destroy(VirtualMachine *vm) {
  // destroy vm
  free(vm);
}

int vm_get_address(VirtualMachine *this, int offset) {
  if (offset < 0) {
    return -offset;
  } else {
    return this->bx + offset;
  }
}

void vm_run(VirtualMachine *this) {
  while (true) {
    const int pc = this->pc;
    const Instruction *inst = &this->cs[pc];
    const int optr = inst->optr;
    int arg1 = vm_get_address(this, inst->arg1);
    int arg2 = vm_get_address(this, inst->arg2);
    int result = vm_get_address(this, inst->result);

    if (pc == -1) {
      break;
    }

    switch (optr) {
    case InsAddBx:
      this->bx += inst->arg1;
      this->pc++;
      break;
    case InsSetBx:
      this->bx = inst->arg1;
      this->pc++;
      break;
    case InsRet:
      this->pc = this->ds[this->bx + 1];
      this->bx -= this->ds[this->bx]; // MinusBx
      break;
    case InsCall:
      this->pc = inst->arg1; // jump to given pc
      break;
    case InsUminus:
      this->ds[result] = -this->ds[arg1];
      this->pc++;
      break;
    case InsOut:
      printf("%d ", this->ds[arg1]);
      this->pc++;
      break;
    case InsIn:
      scanf("%d", &(this->ds[arg1]));
      this->pc++;
      break;
    case InsNot:
    case InsAnd:
    case InsOr:
      printf("not supported yet");
      this->pc++;
      break;
    case InsJne:
      if (this->ds[arg1] != this->ds[arg2])
        this->pc = inst->result;
      else
        this->pc++;
      break;
    case InsJeq:
      if (this->ds[arg1] == this->ds[arg2])
        this->pc = inst->result;
      else
        this->pc++;
      break;
    case InsJge:
      if (this->ds[arg1] >= this->ds[arg2])
        this->pc = inst->result;
      else
        this->pc++;
      break;
    case InsJgt:
      if (this->ds[arg1] > this->ds[arg2])
        this->pc = inst->result;
      else
        this->pc++;
      break;
    case InsJle:
      if (this->ds[arg1] <= this->ds[arg2])
        this->pc = inst->result;
      else
        this->pc++;
      break;
    case InsJlt:
      if (this->ds[arg1] < this->ds[arg2])
        this->pc = inst->result;
      else
        this->pc++;
      break;
    case InsNop:
      this->pc++;
      break;
    case InsMod:
      this->ds[result] = (this->ds[arg1]) % (this->ds[arg2]);
      this->pc++;
      break;
    case InsDiv:
      this->ds[result] = this->ds[arg1] / this->ds[arg2];
      this->pc++;
      break;
    case InsMul:
      this->ds[result] = this->ds[arg1] * this->ds[arg2];
      this->pc++;
      break;
    case InsSub:
      this->ds[result] = this->ds[arg1] - this->ds[arg2];
      this->pc++;
      break;
    case InsAdd:
      this->ds[result] = this->ds[arg1] + this->ds[arg2];
      this->pc++;
      break;
    case InsInit:
      this->ds[arg1] = inst->arg2;
      this->pc++;
      break;
    case InsMov:
      this->ds[arg1] = this->ds[arg2];
      this->pc++;
      break;
    case InsJmp:
      this->pc = inst->result;
      break;
    case InsJtrue:
      if (this->ds[arg1] != 0)
        this->pc = inst->result;
      else
        this->pc++;
      break;
    case InsJfalse:
      if (this->ds[arg1] == 0)
        this->pc = inst->result;
      else
        this->pc++;
      break;
    default:
      printf("Unknown instruction.");
      assert(false);
    }
  }
}

bool vm_load_asm(VirtualMachine *this, const char *filename) {
  FILE *fp = fopen(filename, "r");
  printf("%s\n", filename);
  char line[1024];
  char optr_str[64];
  char *token;
  assert(fp);

  while (fgets(line, sizeof(line), fp)) {
    Instruction* inst = &this->cs[this->pc];
    token = strtok(line, " ");
    sprintf(optr_str, "Ins%s", token);
    inst->optr = str_to_optr(optr_str);
    inst->arg1 = atoi(strtok(NULL, " "));
    inst->arg2 = atoi(strtok(NULL, " "));
    inst->result = atoi(strtok(NULL, " "));

    if (this->pc == 0) {
      this->global_mem_size = this->cs[0].arg1 - 1;
      int bx = this->cs[0].arg1;
      this->ds[bx] = -1;
      this->ds[bx + 1] = -1;
    }
    // printf("%s\n", inst_to_str(&(this->cs[this->pc])));
    this->pc++;
  }
  this->cs_index = this->pc;
  this->pc = 0;
  return true;
}
